package org.zbus.web;

import java.io.File;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SessionManager;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 

public class JettyServer{ 
	private static final Logger log = LoggerFactory.getLogger(JettyServer.class);
	private String webAppDir;
	private int port;
	private String context;
	private int scanIntervalSeconds;
	private boolean running = false;
	private Server server;
	private WebAppContext webApp;
	
	public JettyServer(String webAppDir, int port, String context, int scanIntervalSeconds) {
		if (webAppDir == null)
			throw new IllegalStateException("Invalid webAppDir of web server: " + webAppDir);
		if (port < 0 || port > 65536)
			throw new IllegalArgumentException("Invalid port of web server: " + port);
		if (context == null || "".equals(context.trim()))
			throw new IllegalStateException("Invalid context of web server: " + context);
		
		this.webAppDir = webAppDir;
		this.port = port;
		this.context = context;
		this.scanIntervalSeconds = scanIntervalSeconds;
	}
	
	public void start() {
		if (!running) {
			try {doStart();} catch (Exception e) {log.error(e.getMessage(), e);}
			running = true;
		}
	}
	
	public void stop() {
		if (running) {
			try {server.stop();} catch (Exception e) {log.error(e.getMessage(), e);}
			running = false;
		}
	}
	
	private void doStart() {
		if (!available(port))
			throw new IllegalStateException("port: " + port + " already in use!");
		
		deleteSessionData();
		
		System.out.println("Starting Jetty 8.1.8");
		server = new Server();
		SelectChannelConnector connector = new SelectChannelConnector();
		connector.setPort(port);
		server.addConnector(connector);
		webApp = new WebAppContext();
		webApp.setThrowUnavailableOnStartupException(true);	// 在启动过程中允许抛出异常终止启动并退出 JVM
		webApp.setContextPath(context);
		webApp.setResourceBase(webAppDir);	// webApp.setWar(webAppDir);
		webApp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
		webApp.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false");	// webApp.setInitParams(Collections.singletonMap("org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false"));
		persistSession(webApp);
		
		server.setHandler(webApp);
		changeClassLoader(webApp);
		
		// configureScanner
		if (scanIntervalSeconds > 0) {
			Scanner scanner = new Scanner(PathKit.getRootClassPath(), scanIntervalSeconds) {
				public void onChange() {
					try {
						System.err.println("\nLoading changes ......");
						webApp.stop();
						JettyWebClassLoader loader = new JettyWebClassLoader(webApp, getClassPath());
						webApp.setClassLoader(loader);
						webApp.start();
						System.err.println("Loading complete.");
					} catch (Exception e) {
						System.err.println("Error reconfiguring/restarting webapp after change in watched files");
						log.error(e.getMessage(), e);
					}
				}
			};
			System.out.println("Starting scanner at interval of " + scanIntervalSeconds + " seconds.");
			scanner.start();
		}
		
		try {
			System.out.println("Starting web server on port: " + port);
			server.start();
			System.out.println("Starting Complete.");
			server.join();
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			System.exit(100);
		}
		return;
	}
	
	@SuppressWarnings("resource")
	private void changeClassLoader(WebAppContext webApp) {
		try {
			String classPath = getClassPath();
			JettyWebClassLoader wacl = new JettyWebClassLoader(webApp, classPath);
			wacl.addClassPath(classPath);
		} catch (IOException e) {
			log.error(e.getMessage(), e);
		}
	}
	
	private String getClassPath() {
		return System.getProperty("java.class.path");
	}
	
	private void deleteSessionData() {
		try {
			delete(new File(getStoreDir()));
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private String getStoreDir() {
		String storeDir = PathKit.getWebRootPath() + "/../../session_data" + context;
		if ("\\".equals(File.separator))
			storeDir = storeDir.replaceAll("/", "\\\\");
		return storeDir;
	}
	
	private void persistSession(WebAppContext webApp) {
		String storeDir = getStoreDir();
		
		SessionManager sm = webApp.getSessionHandler().getSessionManager();
		if (sm instanceof HashSessionManager) {
			((HashSessionManager)sm).setStoreDirectory(new File(storeDir));
			return ;
		}
		
		HashSessionManager hsm = new HashSessionManager();
		hsm.setStoreDirectory(new File(storeDir));
		SessionHandler sh = new SessionHandler();
		sh.setSessionManager(hsm);
		webApp.setSessionHandler(sh);
	}
	
	private static boolean available(int port) {
		if (port <= 0) {
			throw new IllegalArgumentException("Invalid start port: " + port);
		}
		
		ServerSocket ss = null;
		DatagramSocket ds = null;
		try {
			ss = new ServerSocket(port);
			ss.setReuseAddress(true);
			ds = new DatagramSocket(port);
			ds.setReuseAddress(true);
			return true;
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (ds != null) {
				ds.close();
			}
			
			if (ss != null) {
				try {
					ss.close();
				} catch (IOException e) {
					// should not be thrown, just detect port available.
					//ignore
				}
			}
		}
		return false;
	}
	
	
	public static void delete(File file) {
		if (file != null && file.exists()) {
			if (file.isFile()) {
				file.delete();
			}
			else if (file.isDirectory()) {
				File files[] = file.listFiles();
				for (int i=0; i<files.length; i++) {
					delete(files[i]);
				}
			}
			file.delete();
		}
	}
	
	public static class PathKit {
		
		private static String webRootPath;
		private static String rootClassPath;
		
		@SuppressWarnings("rawtypes")
		public static String getPath(Class clazz) {
			String path = clazz.getResource("").getPath();
			return new File(path).getAbsolutePath();
		}
		
		public static String getPath(Object object) {
			String path = object.getClass().getResource("").getPath();
			return new File(path).getAbsolutePath();
		}
		
		public static String getRootClassPath() {
			if (rootClassPath == null) {
				try {
					String path = PathKit.class.getClassLoader().getResource("").toURI().getPath();
					rootClassPath = new File(path).getAbsolutePath();
				}
				catch (Exception e) {
					String path = PathKit.class.getClassLoader().getResource("").getPath();
					rootClassPath = new File(path).getAbsolutePath();
				}
			}
			return rootClassPath;
		}
		
		public void setRootClassPath(String rootClassPath) {
			PathKit.rootClassPath = rootClassPath;
		}
		
		public static String getPackagePath(Object object) {
			Package p = object.getClass().getPackage();
			return p != null ? p.getName().replaceAll("\\.", "/") : "";
		}
		
		public static File getFileFromJar(String file) {
			throw new RuntimeException("Not finish. Do not use this method.");
		}
		
		public static String getWebRootPath() {
			if (webRootPath == null)
				webRootPath = detectWebRootPath();
			return webRootPath;
		}
		
		public static void setWebRootPath(String webRootPath) {
			if (webRootPath == null)
				return ;
			
			if (webRootPath.endsWith(File.separator))
				webRootPath = webRootPath.substring(0, webRootPath.length() - 1);
			PathKit.webRootPath = webRootPath;
		}
		
		private static String detectWebRootPath() {
			try {
				String path = PathKit.class.getResource("/").toURI().getPath();
				return new File(path).getParentFile().getParentFile().getCanonicalPath();
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		
		public static boolean isAbsolutelyPath(String path) {
			return path.startsWith("/") || path.indexOf(":") == 1;
		} 
	}
	
	static class JettyWebClassLoader extends WebAppClassLoader {
		private boolean initialized = false;
		
		public JettyWebClassLoader(WebAppContext context, String classPath) throws IOException {
			super(context);
			if(classPath != null){
				String[] tokens = classPath.split(String.valueOf(File.pathSeparatorChar));
				for(String entry : tokens){
					String path = entry;
					if(path.startsWith("-y-") || path.startsWith("-n-")) {
						path = path.substring(3);
					}
					
					if(entry.startsWith("-n-") == false){
						super.addClassPath(path);
					}
				}
			}
			
			initialized = true;
		}
		
		@SuppressWarnings({"unchecked", "rawtypes"})
		public Class loadClass(String name) throws ClassNotFoundException {
			try {
				return loadClass(name, false);
			}
			catch (NoClassDefFoundError e) {
				throw new ClassNotFoundException(name);
			}
		}
		
		public void addClassPath(String classPath) throws IOException {
			if (initialized) {
				if (!classPath.endsWith("WEB-INF/classes/"))
					return;
			}
			super.addClassPath(classPath);
		}
		
		public void addJars(Resource jars) {
			if (initialized) {
				return;
			}
			super.addJars(jars);
		}
	}
	
	public static abstract class Scanner {
		private Logger log = LoggerFactory.getLogger(Scanner.class);
		private Timer timer;
		private TimerTask task;
		private File rootDir;
		private int interval;
		private boolean running = false;
		
		private final Map<String,TimeSize> preScan = new HashMap<String,TimeSize> ();
		private final Map<String,TimeSize> curScan = new HashMap<String,TimeSize> ();
		
		public Scanner(String rootDir, int interval) {
			if (rootDir == null || "".equals(rootDir.trim()))
				throw new IllegalArgumentException("The parameter rootDir can not be blank.");
			this.rootDir = new File(rootDir);
			if (!this.rootDir.isDirectory())
				throw new IllegalArgumentException("The directory " + rootDir + " is not exists.");
			if (interval <= 0)
				throw new IllegalArgumentException("The parameter interval must more than zero.");
			this.interval = interval;
		}
		
		public abstract void onChange();
		
		private void working() {
			scan(rootDir);
			compare();
			
			preScan.clear();
			preScan.putAll(curScan);
			curScan.clear();
		}
		
		private void scan(File file) {
			if (file == null || !file.exists())
				return ;
			
			if (file.isFile()) {
				try {
					curScan.put(file.getCanonicalPath(), new TimeSize(file.lastModified(),file.length()));
				} catch (IOException e) {
					log.error(e.getMessage(), e);
				}
			}
			else if (file.isDirectory()) {
				File[] fs = file.listFiles();
				if (fs != null)
					for (File f : fs)
						scan(f);
			}
		}
		
		private void compare() {
			if (preScan.size() == 0)
				return;
			
			if (!preScan.equals(curScan))
				onChange();
		}
		
		public void start() {
			if (!running) {
				timer = new Timer("JFinal-Scanner", true);
				task = new TimerTask() {public void run() {working();}};
				timer.schedule(task, 1010L * interval, 1010L * interval);
				running = true;
			}
		}
		
		public void stop() {
			if (running) {
				timer.cancel();
				task.cancel();
				running = false;
			}
		}
	}

	static class TimeSize {
		
		final long time;
		final long size;
		
		public TimeSize(long time, long size) {
			this.time = time;
			this.size = size;
		}
		
		public int hashCode() {
			return (int)(time ^ size);
		}
		
		public boolean equals(Object o) {
			if (o instanceof TimeSize) {
				TimeSize ts = (TimeSize)o;
				return ts.time == this.time && ts.size == this.size;
			}
			return false;
		}
		
		public String toString() {
			return "[t=" + time + ", s=" + size + "]";
		} 
	}
	
}






